MyBatis多数据源配置实现读写分离
点击上方 "程序员小乐" ,关注公众号
8点20分,第一时间与你相约
来自:浮生志 | 责编:乐乐 | 链接:https://www.ezlippi.com
图片来自网络
常见的数据库连接池有C3P0、DBCP和阿里巴巴的druid,后两个在实际场景中用的比较多,这个案例简单介绍Spring+Druid+MyBatis 实现多数据源配置,基本原理是继承自Spring提供的AbstractRoutingDataSource这个抽象类,把所有的DataSource放到Map里面,然后重写determineCurrentLookupKey()这个方法,Spring的AbstractRoutingDataSource在获取数据库连接时会先调用determineCurrentLookupKey()方法来找到数据库的key值,然后从Map中找到对应的DataSource获取数据库连接
数据库连接池我用的阿里的druid,首先配置一个数据源的父类,定义一些公共的连接池参数,然后配置了两个继承自AbstractDataSource的
读写datasource,配置如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <!-- applicationContext.xml --> <!-- 数据源参数配置 --> <context:property-placeholder location="classpath:datasource.properties" /> <bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <!-- 基本属性 url、user、password --> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="10"/> <property name="minIdle" value="10"/> <property name="maxActive" value="10"/> <!-- 配置获取连接等待超时的时间 --> <property name="maxWait" value="6000"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="300000"/> <property name="validationQuery" value="SELECT 1"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <!-- 打开PSCache,并且指定每个连接上PSCache的大小 --> <property name="poolPreparedStatements" value="true"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/> <property name="filters" value="config"/> <property name="connectionProperties" value="config.decrypt=false" /> </bean> <bean id="DataSourceRead" parent="abstractDataSource"> <property name="url" value="${read_url}"/> <property name="username" value="${read_userName}"/> <property name="password" value="${read_userValue}"/> </bean> <bean id="DataSourceWrite" parent="abstractDataSource"> <property name="url" value="${write_url}"/> <property name="username" value="${write_userName}"/> <property name="password" value="${write_userValue}"/> </bean> |
然后实现一个类RWDataSource继承自AbstractRoutingDataSource,readDataSource和writeDataSource通过Spring注入,然后重写afterPropertiesSet()和determineCurrentLookupKey()这两个方法,关键代码如下,setter和getter方法我这里省略了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public class RWDataSource extends AbstractRoutingDataSource { private Object writeDataSource; //写数据源 private Object readDataSource; //读数据源 @Override public void afterPropertiesSet() { if (this.writeDataSource == null) { throw new IllegalArgumentException("Property 'writeDataSource' is required"); } setDefaultTargetDataSource(writeDataSource); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(RWDataSourceType.WRITE.name(), writeDataSource); if(readDataSource != null) { targetDataSources.put(RWDataSourceType.READ.name(), readDataSource); } //调用父类的方法把数据源注入 setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { RWDataSourceType dynamicDataSourceGlobal = RWDataSourceHolder.getDataSource(); if(dynamicDataSourceGlobal == null || dynamicDataSourceGlobal == RWDataSourceType.WRITE) { return RWDataSourceType.WRITE.name(); } return RWDataSourceType.READ.name(); } |
determineCurrentLookupKey()方法这里主要是从RWDataSourceHolder这个类里取出RWDataSourceType(枚举类,包含Read和Write),RWDataSourceHolder类里只有一个ThreadLocal的RWDataSourceType对象,用于保存每个线程选择的数据源,在使用Mybatis时要根据执行的SQL语句类型动态修改当前线程的RWDataSourceType。
1 2 3 4 5 6 7 8 9 10 11 | public class RWDataSourceHolder { private static final ThreadLocal<RWDataSourceType> holder = new ThreadLocal<RWDataSourceType>(); public static void putDataSource(RWDataSourceType dataSource){ holder.set(dataSource); } public static RWDataSourceType getDataSource(){ return holder.get(); } } |
接下来就是重点了,怎么根据MyBatis要执行的语句类型来动态修改数据源类型呢,这里就要用到MyBatis提供的插件的能力,MyBatis里的数据库增删改查操作最后都是执行的Executor的query()或者update()方法,因此我们需要做的就是拦截Executor的query和update方法,根据执行的SQL语句类型来动态修改数据源的key值,插件的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | @Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }), @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) }) public class RWDataSourceMybatisPlugin implements Interceptor { private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; private static final Map<String, RWDataSourceType> cacheMap = new ConcurrentHashMap<>(); @Override public Object intercept(Invocation invocation) throws Throwable { Object[] objects = invocation.getArgs(); MappedStatement ms = (MappedStatement) objects[0]; RWDataSourceType dataSourceType = null; if((dataSourceType = cacheMap.get(ms.getId())) == null) { //读方法 if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { //!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库 if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { dataSourceType = RWDataSourceType.WRITE; } else { BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]); String sql = boundSql.getSql().toLowerCase().replaceAll("[\\t\\n\\r]", " "); if(sql.matches(REGEX)) { dataSourceType = RWDataSourceType.WRITE; } else { dataSourceType = RWDataSourceType.READ; } } }else{ dataSourceType = RWDataSourceType.WRITE; } cacheMap.put(ms.getId(), dataSourceType); } //修改当前线程要选择的数据源的key RWDataSourceHolder.putDataSource(dataSourceType); return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } |
最后在mybatis的配置文件里配置拦截插件就可以了,通过以上的步骤就实现了数据库读写分离的功能,有些步骤我省略了,有疑问的可以给我留言,
晚一点我把附件上传上来。
1 2 3 4 5 | <!-- mybatis-config.xml --> <plugins> <plugin interceptor="RWDataSourceMybatisPlugin"> </plugin> </plugins> |
谈谈你对MyBatis的理解?
欢迎在评论区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,或者在学习能力的提升上有新的认识,欢迎转发分享给更多人。
欢迎各位读者加入程序员小乐读者群,在公众号后台回复“加群”或者“学习”即可。
猜你还想看
19个有趣的Linux 命令,最后一个?... 打死我都不敢尝试!
SpringBoot 打包部署,看这篇就够了!
这里有技术、心得、算法、职场、感悟、面经,做一个有趣的帮助程序员成长的公众号。